/**
 * 以滑动方式隐藏元素（模仿JQuery）
 * 
 * @param element 一个 Html 元素
 * @param duration 完成动画的时间
 * @param targetHeight 要制作动画的高度，默认为 0
 */
function slideUp(element: HTMLElement, duration: number = 200, targetHeight = 0, removeStyleAfterFinished: boolean = false) {

  const height = element.offsetHeight;  // 获取元素的当前高度
  element.style.height = height + "px"; // 将元素的高度设置为其当前高度
  element.style.overflow = "hidden";    // 将元素的溢出设置为 hidden 以隐藏元素内的子项目
  const distance = targetHeight - height;  // 计算动画的距离
  const speed = distance / duration;       // 计算动画的速度
  let startTime: number | null = null;     // 设置开始时间

  // 动画函数
  function animation(currentTime: number) {
    // 如果尚未设置开始时间，请进行设置
    if (!startTime) {
      startTime = currentTime;
    }

    const timeElapsed = currentTime - startTime;    // 计算动画开始后经过的时间
    const newHeight = height + speed * timeElapsed; // 计算元素的新高度
    element.style.height = newHeight + "px"; // 将元素的高度设置为新的高度

    // 如果动画尚未完成，请请求另一帧
    if (newHeight > targetHeight) {
      requestAnimationFrame(animation); // 告诉浏览器——你希望执行一个动画，并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。
    } else {
      element.style.height = "0"; // 动画完成后，将元素的高度设置为0，溢出设置为可见
      element.style.overflow = "hidden"; // 元素高度为0后，还要使得其子元素隐藏溢出内容
      if (removeStyleAfterFinished) {
        // 移除由于 slideToggle 相关方法增加的 Style 标签能够复原调用 slideToggle 系列方法产生的副作用
        element.removeAttribute('style');
      }
    }
  }

  // 请求动画的第一帧
  requestAnimationFrame(animation);
}

/**
 * 以滑动方式显示所有隐藏的（模仿JQuery）
 * 
 * @param element 一个 Html 元素
 * @param duration 完成动画的时间
 * @param removeStyleAfterFinished 是否在 slideDown 执行完成后移除内联 style 标签。由于 slideToogle 系列函数是通过注入内联 style 实现的，移除style属性能够还原由 slideToggle 系列函数产生的副作用
 */
function slideDown(element: HTMLElement, duration: number = 200, removeStyleAfterFinished: boolean = true) {
  // 按照与slideUp相同的步骤实现slideDown函数，
  // 不同的是 目标高度 是元素的全高度，距离是 目标高度 和 当前高度 之间的差。
  const height = element.offsetHeight; // 获取元素的当前高度
  // 将元素的高度设置为0，溢出设置为隐藏，为动画做准备
  element.style.height = "0";
  const targetHeight = element.scrollHeight; // 动画的高度。注意 目标高度 是元素的全高度！
  const distance = targetHeight - height; // 动画的距离
  const speed = distance / duration; // 动画的速度
  let startTime: number | null = null; // 设置开始时间

  // 动画函数
  function animation(currentTime: number) {
    // 如果尚未设置开始时间，请进行设置
    if (!startTime) {
      startTime = currentTime;
    }

    const timeElapsed = currentTime - startTime; // 动画开始后经过的时间
    const newHeight = height + speed * timeElapsed; // 元素的新高度
    element.style.height = newHeight + "px"; // 将元素的高度设置为新的高度

    // 如果动画尚未完成，请请求另一帧
    if (newHeight < targetHeight) {
      requestAnimationFrame(animation);
    } else {
      // 将元素的高度设置为其全高，并在动画完成时溢出为可见
      element.style.height = targetHeight + "px";
      if (removeStyleAfterFinished) {
        // 移除由于 slideToggle 相关方法增加的 Style 标签能够复原调用 slideToggle 系列方法产生的副作用
        element.removeAttribute('style');
      }
    }
  }

  // 请求动画的第一帧
  requestAnimationFrame(animation);
}


/**
 * slideUp 和 slideDown 之间切换。
 * 
 * 即：通过检查元素的当前高度并相应地调用 slideUp 或 slideDown 来实现
 * 
 * @param element 一个 Html 元素
 * @param duration 一次动画（执行slideUp 或 slideDown）的时间，默认 200ms
 * @returns 
 * - true 如果执行完 slideUp （意味着由于原先高度大于0，执行了向上关闭）
 * - false 如果执行完 slideDown （意味着执行了向下打开）
 */
function verticalSlideToggle(element: HTMLElement, duration: number = 200): boolean {
  if (element.offsetHeight > 0) {
    slideUp(element, duration, 0);
    return true
  } else {
    slideDown(element, duration);
    return false
  }
}

/**
 * slideUp 和 slideDown 之间切换。
 * 
 * @deprecated 该函数已经改名为 verticalSlideToggle，随时可能删除。因为对应的还有新增的 horizontalSlideToggle 函数，用于 slideLeft 和 slideRight 之间切换。
 */
function slideToggle(element: HTMLElement, duration: number = 200) {
  verticalSlideToggle(element, duration)
}

/**
 * 以滑动方式向左隐藏元素
 * 
 * 
 * @param element 一个 Html 元素
 * @param duration 完成动画的时间
 * @param targetWidth 要制作动画的宽度，默认为 0
 * @param removeStyleAfterFinished 仅当 slideLeft 用作还原时使用（一般而言在Toggle期间，slideLeft、slideRight中进一个用作还原），默认为 false
 */
function slideLeft(element: HTMLElement, duration: number = 200, targetWidth = 0, removeStyleAfterFinished: boolean = false) {
  const width = element.offsetWidth;
  element.style.width = width + 'px';
  element.style.overflow = 'hidden';
  const distance = targetWidth - width;
  const speed = distance / duration;
  let startTime: number | null = null;

  function animation(currentTime: number) {
    if (!startTime) {
      startTime = currentTime;
    }

    const timeElapsed = currentTime - startTime;
    const newWidth = width + speed * timeElapsed;
    element.style.width = newWidth + 'px';

    if (newWidth > targetWidth) {
      requestAnimationFrame(animation);
    } else {
      element.style.width = '0';
      element.style.overflow = 'hidden';
      if (removeStyleAfterFinished) {
        element.removeAttribute('style');
      }
    }
  }

  requestAnimationFrame(animation);
}

/**
 * 以滑动方式向右显示所有隐藏的元素
 * 
 * 
 * @param element 一个 Html 元素
 * @param duration 完成动画的时间
 * @param targetWidth 打开时的宽度，默认为 全宽
 * @param removeStyleAfterFinished 仅当 slideRight 用作还原时使用（一般而言在Toggle期间，slideLeft、slideRight 中进一个用作还原），默认为 false
 */
function slideRight(element: HTMLElement, duration: number = 200, targetWidth: number | null = null, removeStyleAfterFinished: boolean = false) {
  const width = element.offsetWidth; // 获取元素的当前宽度
  element.style.width = '0'; // 将元素的宽度设置为0
  element.style.overflow = 'hidden'; // 将元素的溢出设置为 hidden 以隐藏元素内的子项目
  const target = targetWidth ? targetWidth : element.scrollWidth; // 目标宽度。注意 目标高度 是元素的全高度！
  const distance = target - width; // 计算动画的距离
  const speed = distance / duration; // 计算动画的速度
  let startTime: number | null = null; // 设置开始时间

  // 切换元素的 display 样式为 block
  if (element.style.display === 'none') {
    element.style.display = 'block';
  }

  // 动画函数
  function animation(currentTime: number) {
    // 如果尚未设置开始时间，请进行设置
    if (!startTime) {
      startTime = currentTime;
    }

    const timeElapsed = currentTime - startTime; // 计算动画开始后经过的时间
    const newWidth = width + speed * timeElapsed; // 计算元素的新宽度
    element.style.width = newWidth + 'px'; // 将元素的宽度设置为新的宽度

    // 如果动画尚未完成，请请求另一帧
    if (newWidth < target) {
      requestAnimationFrame(animation); // 告诉浏览器——你希望执行一个动画，并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。
    } else {
      // 将元素的宽度设置为其目标宽，并在动画完成时溢出为可见
      element.style.width = target + 'px';
      element.style.overflow = 'visible';
      if (removeStyleAfterFinished) {
        element.removeAttribute('style');
      }
    }
  }

  // 请求动画的第一帧
  requestAnimationFrame(animation);
}

/**
 * slideLeft 和 slideRight 之间切换。即：通过检查元素的当前宽度并相应地调用 slideLeft 或 slideRight 来实现
 * @param element 一个 Html 元素
 * @param duration 一次动画（执行 slideLeft 或 slideReft）的时间，默认 200ms
 * @param targetWidth 打开时 slideRight 的目标宽度，默认为 全宽
 * @returns  返回一个 boolean 值，代表切换后元素是关闭还是打开状态：
 * - true：关闭，即执行 slideLeft
 * - false：打开，即执行 slideRight
 */
function horizontalSlideToggle(element: HTMLElement, duration: number = 200, targetWidth: number | null = null, restoreBy: 'Left' | 'Right' = 'Right'): boolean {
  if (restoreBy === 'Right') {
    if (element.offsetWidth > 0) {
      slideLeft(element, duration, 0, false);
      return true;
    } else {
      slideRight(element, duration, targetWidth, true);
      return false;
    }
  } else {
    if (element.offsetWidth > 0) {
      slideRight(element, duration, targetWidth, false);

      return true;
    } else {
      slideLeft(element, duration, 0, true);
      return false;
    }
  }
}

export {
  slideUp,
  slideDown,
  verticalSlideToggle,
  slideToggle,
  slideLeft,
  slideRight,
  horizontalSlideToggle
};
